Advance Features
Decorators
A decorator is a special kind of declaration that can be attached to:
- Classes
- Methods
- Properties
- Parameters
They are functions that take metadata about the thing they decorate and optionally modify it.
You must enable them in tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
General Syntax
A decorator is just a function, prefixed with @ when used:
function MyDecorator(target: any) {
console.log("Decorator called on:", target);
}
@MyDecorator
class Example {}
Class Decorators
Applied to a class declaration.
They receive the class constructor as their only argument.
function Logger(constructor: Function) {
console.log(`Class ${constructor.name} is created`);
}
@Logger
class Person {
constructor(public name: string) {}
}
Output when class is defined:
Class Person is created
@Loggerruns when the class is declared, not when instantiated.- Can be used for logging, dependency injection, or modifying the class.
Modifying a Class
function Seal(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@Seal
class Car {
model = "Tesla";
}
Now the class and its prototype are sealed (no new properties can be added).
Method Decorators
Applied to a method inside a class.
They receive:
- The prototype of the class (or constructor for static methods).
- The method name.
- The property descriptor (can be modified).
function LogMethod(
target: any,
propertyName: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyName} with`, args);
return originalMethod.apply(this, args);
};
}
class Calculator {
@LogMethod
add(a: number, b: number) {
return a + b;
}
}
const calc = new Calculator();
console.log(calc.add(2, 3));
Output:
Calling add with [ 2, 3 ]
5
- We intercepted the method call.
- Useful for logging, caching, validation, performance measurement.
Property Decorators
Applied to class properties.
They can:
- Observe or modify metadata about properties.
- They cannot directly change property values (that happens at runtime).
function ReadOnly(target: any, propertyName: string) {
Object.defineProperty(target, propertyName, {
writable: false
});
}
class User {
@ReadOnly
role = "Admin";
}
const u = new User();
u.role = "User"; // ❌ Error in strict mode, role is read-only
Adding Metadata
function Format(formatString: string) {
return function (target: any, propertyName: string) {
let value: string;
const getter = () => value;
const setter = (newVal: string) => {
value = `${formatString} ${newVal}`;
};
Object.defineProperty(target, propertyName, {
get: getter,
set: setter
});
};
}
class Product {
@Format("SKU")
code: string = "";
}
const p = new Product();
p.code = "123";
console.log(p.code); // "SKU 123"
Parameter Decorators
Applied to method parameters. They let you observe metadata about parameters but cannot directly change their values.
function LogParameter(target: any, methodName: string, parameterIndex: number) {
console.log(`Parameter #${parameterIndex} in method ${methodName}`);
}
class Greeter {
greet(@LogParameter message: string) {
console.log("Hello,", message);
}
}
Output when class is defined
Parameter #0 in method greet
Summary of Decorators
| Decorator Type | Target | Use Case |
|---|---|---|
| Class Decorator | Class constructor | Logging, extending, dependency injection |
| Method Decorator | Method descriptor | Logging, security, caching |
| Property Decorator | Class property | Metadata, validation, formatting |
| Parameter Decorator | Method parameter | Metadata for parameters (e.g., DI, validation) |
Mixins
A Mixin is a way to combine reusable pieces of functionality into classes without using traditional inheritance (i.e., extends).
- Unlike classical inheritance (single base class), mixins allow you to compose multiple behaviors into a single class.
- They help achieve multiple inheritance–like behavior in TypeScript.
Think of them like "function helpers" that add behavior to classes."
Why Use Mixins?
- Code Reuse → Reuse functionality across multiple classes.
- Avoid Deep Inheritance → Instead of building tall class hierarchies, you "mix in" behavior.
- Composition over Inheritance → A common design principle.
How Mixins Work in TypeScript
TypeScript doesn’t directly support multiple inheritance (like in C++), but you can use mixins with a special pattern:
- Create classes (or traits) that define behaviors.
- Use a helper type (
Constructor) to allow extending. - Use
Object.assignto mix functionality into the target class.
Example of Mixins
- Define a Constructor Type
type Constructor<T = {}> = new (...args: any[]) => T;
This is a generic constructor type — it allows mixins to accept any class as a base. 2. Create Mixins
// A mixin for adding logging capability
function Logger<TBase extends Constructor>(Base: TBase) {
return class extends Base {
log(message: string) {
console.log(`[LOG]: ${message}`);
}
};
}
// A mixin for adding timestamp capability
function Timestamp<TBase extends Constructor>(Base: TBase) {
return class extends Base {
getTimestamp() {
return new Date().toISOString();
}
};
}
Loggeradds a.log()method.Timestampadds a.getTimestamp()method.- Both are reusable and can be applied to any class.
- Apply Mixins
// Base class
class Person {
constructor(public name: string) {}
}
// Apply mixins (Person -> Logger -> Timestamp)
class Employee extends Logger(Timestamp(Person)) {
constructor(name: string, public role: string) {
super(name);
}
}
- Use It
const emp = new Employee("Alice", "Developer");
console.log(emp.name); // Alice
console.log(emp.role); // Developer
emp.log("Started working!"); // [LOG]: Started working!
console.log(emp.getTimestamp()); // e.g. 2025-09-03T13:45:10.123Z
Employee class now inherits multiple behaviors via mixins.
Key Notes
- Mixins are composable → You can chain them in different orders.
- Order matters → The last mixin in the chain overrides previous ones if they define the same method.
- Better than multiple inheritance → Avoids diamond problem in C++.
- Helps code reuse → Without bloating single base classes.
keyof, typeof, and infer
keyof
keyof is a type operator that takes an object type and produces a union of its keys as string (or number) literal types.
type Person = {
name: string;
age: number;
isAdmin: boolean;
};
type PersonKeys = keyof Person;
// PersonKeys = "name" | "age" | "isAdmin"
So keyof extracts "name" | "age" | "isAdmin".
Usage with Generics
You can use it to ensure function parameters are valid property names:
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person: Person = { name: "Alice", age: 25, isAdmin: true };
const name = getValue(person, "name"); // ✅ string
const age = getValue(person, "age"); // ✅ number
// getValue(person, "salary"); ❌ Error: "salary" is not a key of Person
keyof helps us enforce type safety when accessing object properties.
typeof
typeof in TypeScript has two meanings:
- Runtime JavaScript operator → Returns the type of a value as a string (e.g.,
"string","number"). - TypeScript type operator → Gets the type of a value/variable so you can reuse it in type annotations.
let person = {
name: "Alice",
age: 25,
isAdmin: true,
};
type PersonType = typeof person;
// PersonType = { name: string; age: number; isAdmin: boolean }
infer
infer is a keyword used in conditional types that allows TypeScript to "infer" a type variable inside a type definition.
It’s often used when building utility types.
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // A = string
type B = UnwrapPromise<number>; // B = number
- If
Tis aPromise<U>, then inferU. - Otherwise, just return
T.
Example with Arrays:
type ElementType<T> = T extends (infer U)[] ? U : T;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<boolean>; // boolean
infer lets us extract inner types from complex structures.
Template Literal Types
Template Literal Types in TypeScript are similar to JavaScript template strings (${...}), but they work at the type level.
They allow you to create new string literal types by combining unions, literals, or interpolating types into strings.
type Role = "admin" | "user" | "guest";
type RoleMessage = `Welcome, ${Role}`;
type RoleMessage = "Welcome, admin" | "Welcome, user" | "Welcome, guest";
String Manipulation with Built-in Helpers
TypeScript has utility types that work with template literals: Uppercase<T>, Lowercase<T>, Capitalize<T>, Uncapitalize<T>
type Greeting = "hello";
type Shout = Uppercase<Greeting>; // "HELLO"
type Capital = Capitalize<Greeting>; // "Hello"
Indexed Access Types
An Indexed Access Type lets you retrieve the type of a specific property (or set of properties) from another type using the T[K] syntax.
👉 If you know how you access object properties at runtime with obj["key"], indexed access types do the same thing at the type level.
type Person = {
name: string;
age: number;
isAdmin: boolean;
};
type NameType = Person["name"]; // string
type AgeType = Person["age"]; // number
type AdminType = Person["isAdmin"]; // boolean
Union of Keys
You can pass multiple keys (a union) to extract multiple property types:
type PersonInfo = Person["name" | "age"];
// string | number
Using keyof with Indexed Access
You can use keyof to extract all property types from an object:
type PersonValues = Person[keyof Person];
// string | number | boolean